package com.lam.common.utils.http;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;

import com.lam.common.exception.BusinessException;
import com.lam.common.utils.JsonUtil;
import com.lam.common.utils.NumberUtil;
import com.lam.common.utils.StringUtils;

@SuppressWarnings("unchecked")
public class HttpClientUtils {

    private static final Logger logger = LoggerFactory.getLogger(HttpClientUtils.class);
    
    public static final String CHARSET = "UTF-8";
    public static final int MAX_LIMIT = 1000;
    
    private static PoolingHttpClientConnectionManager cm = null;
    private static RequestConfig globalRequestConfig = null;
    
    private static final RequestInfo requestInfo = new RequestInfo(); 
    
	static {

		Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
				.register("http", PlainConnectionSocketFactory.getSocketFactory())
				.register("https", HttpFactoryUtil.getDefaultSocketFactory()).build();
		cm = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
		// 连接池最大连接数
		cm.setMaxTotal(300);
		// 单条链路最大连接数（一个ip+一个端口 是一个链路）
		cm.setDefaultMaxPerRoute(50);
		
		globalRequestConfig = RequestConfig.custom()
				  .setSocketTimeout(120000)
				  .setConnectTimeout(120000)
				  .setConnectionRequestTimeout(120000).build();
	}
    
    private HttpClientUtils() {
        throw new IllegalStateException("Utility class");
    }
    
    public static void setRequestConfig(RequestConfig config){
    	if(config != null){
    		globalRequestConfig = config;
    	}
    }
    
    public static String requestName(String name){
    	requestInfo.put("name", name);
    	return name;
    }
    
    public static String requestName(){
    	return requestInfo.get("name");
    }
    
    public static String uid() {
    	String id = requestInfo.get("uid");
    	if(StringUtils.isBlank(id)){
    		id = getUid();
    		uid(id);
    	}
    	return id;
    }

    public static String getUid() {
		return String.valueOf(System.currentTimeMillis()*MAX_LIMIT + RandomUtils.nextInt(0, MAX_LIMIT)).substring(3);
	}
    
    public static String uid(String uid) {
    	requestInfo.put("uid", uid);
		return uid;
	}
    
    public static long startRequest() {
    	long start = System.currentTimeMillis();
    	requestInfo.put("startTime", String.valueOf(start));
    	return start;
    }
    
    public static long startTime() {
    	return Long.valueOf(requestInfo.get("startTime"));
    }
    
    public static boolean usePool() {
    	String usePool = requestInfo.get("usePool");
    	return StringUtils.isBlank(usePool) ? true : Boolean.valueOf(usePool);
    }
    
    public static boolean usePool(boolean usePool) {
    	requestInfo.put("usePool", String.valueOf(usePool));
    	return usePool;
    }
    
    public static int printResultLength() {
    	String printResultLength = requestInfo.get("printResultLength");
    	return NumberUtil.toInt(printResultLength, 0);
    }
    
    public static int printResultLength(int printResultLength) {
    	requestInfo.put("printResultLength", String.valueOf(printResultLength));
    	return printResultLength;
    }
    
    public static boolean usePoolRetry() {
    	String usePoolRetry = requestInfo.get("usePoolRetry");
    	return StringUtils.isBlank(usePoolRetry) ? true : Boolean.valueOf(usePoolRetry);
    }
    
    public static boolean usePoolRetry(boolean usePool) {
    	requestInfo.put("usePoolRetry", String.valueOf(usePool));
    	return usePool;
    }
    
    public static void uidClear(boolean isClear) {
    	requestInfo.put("uidClear", String.valueOf(isClear));
	}
    
    public static void removeRequestInfo(String key){
    	requestInfo.remove(key);
    }
    public static void removeAllRequestInfo(boolean check){
    	String uid = null;
    	if(check && "false".equalsIgnoreCase(requestInfo.get("uidClear"))){
    		uid = requestInfo.get("uid");
    	}
    	requestInfo.remove(null);
    	if(StringUtils.isNotBlank(uid)){
    		uid(uid);
    	}
    }
    
    private static CloseableHttpClient getPoolHttpClient(boolean usePool) {
    	if(usePool){
    		return HttpClients.custom().setConnectionManager(cm).setKeepAliveStrategy(HttpFactoryUtil.getConnectionKeepAliveStrategy()).build();
    	}else{
    		return HttpClients.createDefault();
    	}
    }
	
    /**
     * 下载网络文件
     * @param httpUrl
     * @param fileName
     * @return
     */
    public static void downloadFile(String httpUrl, String fileName) throws IOException {

        URL url = null;
        try {
            url = new URL(httpUrl);
        } catch (MalformedURLException e) {
            logger.error(e.getMessage(), e);
            logger.error("下载链接错误, url:{}", httpUrl);
            throw new BusinessException("请检查下载链接是正确", e);
        }
        
        File file = new File(getFileDir(fileName));
        if(!file.exists()){
            file.mkdirs();
        }
        
        try(FileOutputStream fs = new FileOutputStream(fileName);) {
            URLConnection conn = url.openConnection();
            InputStream inStream = conn.getInputStream();
            
            byte[] buffer = new byte[1024];
            int byteReadCount;
            while ((byteReadCount = inStream.read(buffer)) != -1) {
                fs.write(buffer, 0, byteReadCount);
            }
            logger.debug("文件下载完成! fileName:{}", fileName);
        } catch (FileNotFoundException e) {
        	logger.error("不存在的目录或文件, fileName:{}", fileName);
            logger.error(e.getMessage(), e);
            throw new BusinessException("不存在的目录或文件："+fileName, e);
        } catch (IOException e) {
        	logger.error("文件下载失败, url:{}, fileName:{}", httpUrl, fileName);
            logger.error(e.getMessage(), e);
            throw e;
        }
    }

    private static String getFileDir(String fileName){
    	int i1 = fileName.lastIndexOf('/');
		int i2 = fileName.lastIndexOf('\\');
        return fileName.substring(0, (i1 > i2 ? i1 : i2));
    }

    public static void close(Closeable c){
        if(c != null){
            try {
                c.close();
            } catch (IOException e) {
                logger.error(e.getMessage(), e);
            }
        }
    }
    
    public static void release(HttpRequestBase httpRequestBase){
    	if(httpRequestBase != null){
			httpRequestBase.releaseConnection();
    	}
    }
    
    private static Map<String, String> addDefaultHeader(Map<String, String> header){
    	if(header == null){
    		header = new HashMap<>();
    	}
    	if(!header.containsKey("Content-Type")){
    		header.put("Content-Type", MediaType.APPLICATION_JSON_VALUE);
    	}
    	if(!header.containsKey("Accept-Charset")){
    		header.put("Accept-Charset", CHARSET);
    	}
    	if(!header.containsKey("accept")){
    		header.put("accept", "*/*");
    	}
    	if(!header.containsKey("connection")){
    		header.put("connection", "Keep-Alive");
    	}
    	if(!header.containsKey("user-agent")){
    		header.put("user-agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36");
    	}
    	return header;
    }
    
	private static <T> ResponseHandler<T> newResponseHandler(final Class<T> clazz) {
		return new ResponseHandler<T>() {
			@Override
			public T handleResponse(HttpResponse response) throws IOException {
				String responseString = IOUtils.toString(response.getEntity().getContent(), CHARSET);
				long timeCost = System.currentTimeMillis() - startTime() ;
				if(StringUtils.isBlank(responseString)){
					printResult(responseString, timeCost);
					return null;
				}
				try {
					if(clazz == null || String.class == clazz){
						printResult(responseString, timeCost);
						return (T)responseString;
					}
					responseString = JsonUtil.toJsonString(JsonUtil.parseJSON(responseString), true);
					printResult(responseString, timeCost);
					if(Map.class == clazz){
						return (T)JsonUtil.parseJSON(responseString);
					}
					return JsonUtil.toJavaObject(responseString, clazz);
				} catch (Exception e) {
					printResult(responseString, timeCost);
					logger.error(MessageFormat.format("uid:{0}, requestName:{1}, 返回结果解析异常:{2}", 
							uid(), requestName(), e.getMessage()), e);
					throw new IOException("返回结果解析异常");
				}
			}
		};
	}
	
	private static void printResult(String response, long timeCost){
		String uid = uid();
		String requestName = requestName();
		int resultLength = printResultLength();
		String resultString = resultLength < 1 ? response : String.format("%s...(Length:%s)", StringUtils.substring(response, 0, resultLength), response.length());
		logger.info("uid:{}, time cost:{}ms, requestName:{}, 响应结果:{}", uid, timeCost, requestName, StringUtils.substring(resultString, 0, 100));
	}
	
	private static void printErrorResult(String errorMsg, long timeCost){
		String uid = uid();
		String requestName = requestName();
		logger.info("uid:{}, time cost:{}ms, requestName:{}, 异常信息:{}", 
				uid, timeCost, requestName, errorMsg);
	}
	
	private static <T> T execute(HttpRequestBase method, Class<T> clazz) throws IOException{
		
		try {
			startRequest();
			return getPoolHttpClient(usePool()).execute(method, newResponseHandler(clazz));
		} catch (Exception e) {
			long timeCost = System.currentTimeMillis() - startTime() ;
			printErrorResult(e.getClass() + ": " + e.getMessage(), timeCost);
			if(e instanceof IOException) {
				throw e;
			}
			if(checkRetry(e)){
				logger.error(MessageFormat.format("uid:{0}, requestName:{1}, 使用httpPool请求失败将不使用httpPool重试一次, Exception {2}", 
						uid(), requestName(), e.getClass() + ": " + e.getMessage()));
				try {
					startRequest();
					return getPoolHttpClient(false).execute(method, newResponseHandler(clazz));
				} catch (Exception e1) {
					if(e instanceof IOException) {
						throw e;
					}
					throw new BusinessException(MessageFormat.format("uid:{0}, requestName:{1}, {2}", uid(), requestName(), e1.getMessage()), e1);
				}
	    	}
			throw new BusinessException(MessageFormat.format("uid:{0}, requestName:{1}, {2}", uid(), requestName(), e.getMessage()), e);
		} finally {
			release(method);
			removeAllRequestInfo(true);
		}
	}
	
	private static boolean checkRetry(Exception e){
		if(!usePool() || !usePoolRetry()){
			return false;
		}
		if(e instanceof NoHttpResponseException){
			return true;
		}
		return e instanceof SocketException && StringUtils.contains(e.getMessage(), "Connection reset");
	}
	
	private static String assembleParam(HttpRequestBase method, String url, Map<String, String> header, RequestConfig config) throws IOException{
		if(StringUtils.isBlank(url)){
			logger.error("http请求URL不能为空!");
			throw new IOException("http请求URL不能为空!");
		}
		
		method.setConfig(config != null ? config : globalRequestConfig);
		method.setURI(URI.create(url));
		
		if(!CollectionUtils.isEmpty(header)){
			for (Entry<String, String> entries : header.entrySet()) {
				method.addHeader(entries.getKey(), entries.getValue());
			}
			return JsonUtil.toJsonString(header);
		}
		return "{}";
	}
	
	public static <T> T callWithBodyForm(String url, HttpEntityEnclosingRequestBase method, Map<String, Object> bodyForm, 
			Map<String, String> header, Class<T> clazz, RequestConfig config) throws IOException {
		
		header = addDefaultHeader(header);
		header.put("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE);
		String headerStr = assembleParam(method, url, header, config);
		
		String body = "";
		if(!CollectionUtils.isEmpty(bodyForm)){
			body = JsonUtil.toJsonString(bodyForm, true);
			List<NameValuePair> param = new ArrayList<>(bodyForm.size());
			for (Entry<String, Object> entry : bodyForm.entrySet()) {
				if(StringUtils.isNotBlank(entry.getKey())){
					param.add(new BasicNameValuePair(entry.getKey(), entry.getValue() == null ? "" : entry.getValue().toString()));
				}
			}
			method.setEntity(new UrlEncodedFormEntity(param, CHARSET));
		}
		
		printBodyRequest(url, method, headerStr, body);
		return execute(method, clazz);
	}

	private static void printBodyRequest(String url, HttpEntityEnclosingRequestBase method, 
			String headerStr, String body) {
		String uid = uid();
		String requestName = requestName();
		logger.info("uid:{}, requestName:{}, request method:{}, url:{}, 请求参数body:{}, header:{}", 
				uid, requestName, method.getMethod(), url, body, headerStr);
	}

	public static <T> T callWithBody(String url, HttpEntityEnclosingRequestBase method, Object requestBody, 
			Map<String, String> header, Class<T> clazz, RequestConfig config) throws IOException {
		
		String headerStr = assembleParam(method, url, addDefaultHeader(header), config);
		
		String body = "";
		if(requestBody != null){
			body = requestBody.getClass() == String.class?(String)requestBody
					:JsonUtil.toJsonString(requestBody, true);
			method.setEntity(new StringEntity(body, CHARSET));
		}
		
		printBodyRequest(url, method, headerStr, body);
		return execute(method, clazz);
	}
	
	public static <T> T callWithGet(String url, Map<String, Object> param, Map<String, String> header, 
			Class<T> clazz, RequestConfig config) throws IOException {
		
		HttpGet method = new HttpGet();
		url = addUrlParam(url, param);
		String headerStr = assembleParam(method, url, addDefaultHeader(header), config);
		
		String uid = uid();
		String requestName = requestName();
		logger.info("uid:{}, requestName:{}, request method:{}, url:{}, header:{}", 
				uid, requestName, method.getMethod(), url, headerStr);
		return execute(method, clazz);
	}

	// POST FORM 请求方法 -- start --  
	/**
	 * @param url
	 * @param requestBody
	 * @param header
	 * @param clazz 返回结果对象类
	 * @param config RequestConfig
	 * @return
	 * @throws IOException
	 */
	public static <T> T postForm(String url, Map<String, Object> requestBody, Map<String, String> header, Class<T> clazz, RequestConfig config) throws IOException {
		return callWithBodyForm(url, new HttpPost(), requestBody, header, clazz, config);
	}
	
	/**
	 * @param url
	 * @param requestBody
	 * @param header
	 * @param clazz 返回结果对象类
	 * @return
	 * @throws IOException
	 */
	public static <T> T postForm(String url, Map<String, Object> requestBody, Map<String, String> header, Class<T> clazz) throws IOException {
		return callWithBodyForm(url, new HttpPost(), requestBody, header, clazz, null);
	}
	
	public static <T> T postForm(String url, Map<String, Object> requestBody, Class<T> clazz) throws IOException {
		return postForm(url, requestBody, null, clazz);
	}
	
	public static Map<String, Object> postForm(String url, Map<String, Object> requestBody, Map<String, String> header) throws IOException {
		return postForm(url, requestBody, header, Map.class);
	}
	
	public static Map<String, Object> postForm(String url, Map<String, Object> requestBody) throws IOException {
		return postForm(url, requestBody, null, Map.class);
	}
	// POST FORM 请求方法 -- end -- 
	
	// POST请求方法 -- start --  
	/**
	 * @param url
	 * @param requestBody
	 * @param header
	 * @param clazz 返回结果对象类
	 * @param config RequestConfig
	 * @return
	 * @throws IOException
	 */
	public static <T> T post(String url, Object requestBody, Map<String, String> header, Class<T> clazz, RequestConfig config) throws IOException {
		return callWithBody(url, new HttpPost(), requestBody, header, clazz, config);
	}
	
	/**
	 * @param url
	 * @param requestBody
	 * @param header
	 * @param clazz 返回结果对象类
	 * @return
	 * @throws IOException
	 */
	public static <T> T post(String url, Object requestBody, Map<String, String> header, Class<T> clazz) throws IOException {
		return callWithBody(url, new HttpPost(), requestBody, header, clazz, null);
	}
	
	public static <T> T post(String url, Object requestBody, Class<T> clazz) throws IOException {
		return post(url, requestBody, null, clazz);
	}
	
	public static Map<String, Object> post(String url, Object requestBody, Map<String, String> header) throws IOException {
		return post(url, requestBody, header, Map.class);
	}
	
	public static Map<String, Object> post(String url, Object requestBody) throws IOException {
		return post(url, requestBody, null, Map.class);
	}
	
	public static String postData(String url, Object requestBody) throws IOException {
		return post(url, requestBody, null, String.class);
	}
	
	// POST请求方法 -- end -- 

	// PUT请求方法 -- start --  
	/**
	 * @param url
	 * @param requestBody
	 * @param header
	 * @param clazz 返回结果对象类
	 * @param config RequestConfig
	 * @return
	 * @throws IOException
	 */
	public static <T> T put(String url, Object requestBody, Map<String, String> header, Class<T> clazz, RequestConfig config) throws IOException {
		return callWithBody(url, new HttpPut(), requestBody, header, clazz, config);
	}
	
	public static <T> T put(String url, Object requestBody, Map<String, String> header, Class<T> clazz) throws IOException {
		return callWithBody(url, new HttpPut(), requestBody, header, clazz, null);
	}
	
	public static <T> T put(String url, Object requestBody, Class<T> clazz) throws IOException {
		return put(url, requestBody, null, clazz);
	}
	
	public static Map<String, Object> put(String url, Object requestBody, Map<String, String> header) throws IOException {
		return put(url, requestBody, header, Map.class);
	}
	
	public static Map<String, Object> put(String url, Object requestBody) throws IOException {
		return put(url, requestBody, null, Map.class);
	}
	// PUT请求方法 -- end --
	
	// GET请求方法 -- start --  
	/**
	 * @param url
	 * @param param
	 * @param header
	 * @param clazz 返回结果对象类
	 * @param config RequestConfig
	 * @return
	 * @throws IOException
	 */
	public static <T> T get(String url, Map<String, Object> param, Map<String, String> header, Class<T> clazz, RequestConfig config) throws IOException {
		
		return callWithGet(url, param, header, clazz, config);
	}
	
	public static <T> T get(String url, Map<String, Object> param, Map<String, String> header, Class<T> clazz) throws IOException {
		
		return callWithGet(url, param, header, clazz, null);
	}
	
	public static <T> T get(String url, Map<String, Object> param, Class<T> clazz) throws IOException {
		
		return get(url, param, null, clazz);
	}
	
	public static Map<String, Object> get(String url, Map<String, Object> param, Map<String, String> headers) throws IOException {
		return get(url, param, headers, Map.class);
	}
	
	public static Map<String, Object> get(String url, Map<String, Object> param) throws IOException {
		return get(url, param, null, Map.class);
	}
	
	public static Map<String, Object> get(String url) throws IOException {
		return get(url, null, null, Map.class);
	}
	// GET请求方法 -- end -- 

	/**
	 * 向URL添加查询参数
	 * @param url
	 * @param param
	 * @return
	 */
	public static String addUrlParam(String url, Map<String, Object> param){
		if(StringUtils.isBlank(url)){
			return StringUtils.EMPTY; 
		}
		String paramStr = serialize(param);
		if(StringUtils.isNotBlank(paramStr)){
			url += url.contains("?") ? "&" : "?";
			url += paramStr;
		}
		return url;
	}
	
	/**
	 * 序列化参数
	 * @param param
	 * @return
	 */
	public static String serialize(Map<String, Object> param) {
		if(CollectionUtils.isEmpty(param)){
			return StringUtils.EMPTY;
		}
		StringBuilder paramStr = new StringBuilder();
		Set<Entry<String, Object>> set = param.entrySet();
		int i = 0;
		for (Entry<String, Object> entry : set) {
			if(StringUtils.isBlank(entry.getKey()) || entry.getValue() == null){
				continue;
			}
			if(i > 0){
				paramStr.append("&");
			}
			paramStr.append(entry.getKey()).append("=").append(entry.getValue());
			i++;
		}
		return paramStr.toString();
	}

	private static class RequestInfo{
		
		private ThreadLocal<Map<String, String>> requestMap = new ThreadLocal<>();
		
		public Map<String, String> getMap(){
			Map<String, String> map = requestMap.get();
			if(map == null){
				map = new HashMap<>();
	    		requestMap.set(map);
			}
			return map;
		}
		
		public String get(String key){
			
			String v = getMap().get(key);
	    	return v == null?"":v;
		}
		
		public void put(String key, String value){
			getMap().put(key, value);
		}
		
	    public void remove(String key){
	    	if(key == null){
	    		requestMap.remove();
	    	}else{
	    		getMap().remove(key);
	    	}
	    }
	}
	
}
